// Copyright (c) Microsoft Corporation. All rights reserved.  
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.IO;
using System.Security.Policy;
using System.Reflection;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Specialized;
using System.Xml;
using System.Runtime.InteropServices;
using System.Xml.XPath;
using System.Configuration;
using System.Threading;
using System.ComponentModel;
using Microsoft.Win32;

namespace Microsoft.Tools.CodeRunner.Csr
{
	internal enum MessageType
	{
		Normal, 
		Warning, 
		Error,
		Debug
	};

	internal class ProgramOptions
	{
		#region Constructor
		public ProgramOptions()
		{
			try
			{	
				ListDictionary environ = ParseEnvironment();
				Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
			
				debug = GetBoolSetting(config, environ, "DebugMessages", false);
				cacheImages = GetBoolSetting(config, environ, "CacheImages", true);
				searchSystemPath = GetBoolSetting(config, environ, "SearchSystemPath", true);
				allowUnsafeCode = GetBoolSetting(config, environ, "AllowUnsafeCode", false);
			}
			catch (ConfigurationErrorsException)
			{
				Program.WriteMessage(MessageType.Warning, "Application configuration could not be loaded");
			}
		}
		
		#endregion

		#region Instance Properties
		
		[CommandLineArgument("help", Description="HelpSwitchDescription", ShortName="?")]
		public bool ShowHelp
		{
			get 
			{
				return showHelp;
			}
			set
			{
				showHelp = value;
			}
		}
		
		[CommandLineArgument("reference", Description="ReferenceSwitchDescription", ShortName="r", ValueHint="ReferenceSwitchHint")]
		public ParsedPath[] References
		{
			get
			{
				return references;
			}
			set
			{
				references = value;
			}
		}
		
		[DefaultCommandLineArgument("script", ValueHint="ScriptSwitchHint")]
		public ParsedPath ScriptPath
		{
			get { return scriptPath; }
			set { scriptPath = value; }
		}

		[UnprocessedCommandLineArgument("arguments", ValueHint="ArgumentSwitchHint")]
		public string[] Arguments
		{
			get { return arguments; }
			set { arguments = value; }
		}

		public bool Debug
		{
			get { return debug; }
		}
		
		public bool CacheImages
		{
			get { return cacheImages; }
		}

		public bool SearchSystemPath
		{
			get { return searchSystemPath; }
		}

		public bool AllowUnsafeCode
		{
			get { return allowUnsafeCode; }
		}

		#endregion

		#region Class Methods
		
		// Read the CSR_CONFIG environment variable to allow for override of settings in the .config file
		private static ListDictionary ParseEnvironment()
		{
			string allVars = Environment.GetEnvironmentVariable("CSR_CONFIG");
			
			if (allVars == null)
				return null;	
				
			string[] vars = allVars.Split(';');

			ListDictionary environ = null;
			
			if (vars.Length > 0)
			{
				environ = new ListDictionary();
			
				foreach (string var in vars)
				{
					string[] parts = var.Split('=');
					
					if (parts.Length == 1)
					{
						environ[parts[0].Trim()] = String.Empty;
					}
					else if (parts.Length == 2 && parts[0].Length > 0)
					{
						environ[parts[0].Trim()] = parts[1].Trim();
					}
				}
			}
			
			return environ;
		}

		private static bool GetBoolSetting(Configuration config, ListDictionary environ, string setting, bool valueDefault)
		{
			bool val = valueDefault;
			
			try 
			{ 
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting];

					if (element != null)
					{
						val = (CaseInsensitiveComparer.Default.Compare(element.Value, CsrResources.No) != 0 &&
							CaseInsensitiveComparer.Default.Compare(element.Value, CsrResources.False) != 0);
					}
				}
				
				if (environ != null)
				{
					string temp = (string)environ[setting];
					
					if (!String.IsNullOrEmpty(temp))
					{
						val = (CaseInsensitiveComparer.Default.Compare(temp, CsrResources.No) != 0 &&
							CaseInsensitiveComparer.Default.Compare(temp, CsrResources.False) != 0);
					}
				}
			}
			catch (ConfigurationException e) 
			{
				Program.WriteMessage(MessageType.Warning, e.Message);	
			}
			
			return val;
		}

		private static string GetStringSetting(Configuration config, ListDictionary environ, string setting)
		{
			string val = null;
		
			try 
			{ 
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting]; 
					
					if (element != null)
					{
						val = element.Value;
					}
				}

				if (environ != null)
				{
					string temp = (string)environ[setting];

					if (!String.IsNullOrEmpty(temp))
					{
						val = temp;
					}
				}
			}
			catch (ConfigurationException e) 
			{
				Program.WriteMessage(MessageType.Warning, e.Message);	
			}

			if (val != null && val.CompareTo(String.Empty) == 0)
				val = null;
			
			return val;
		}

		private int GetInt32Setting(Configuration config, ListDictionary environ, string setting, int defVal)
		{
			int val = defVal;
		
			try 
			{ 
				if (config != null)
				{
					KeyValueConfigurationElement element = config.AppSettings.Settings[setting];

					if (element != null)
					{
						val = Int32.Parse(element.Value);
					}
				}

				if (environ != null)
				{
					string temp = (string)environ[setting];

					if (!String.IsNullOrEmpty(temp))
					{
						val = Int32.Parse(temp);
					}
				}
			}
			catch (ConfigurationException e) 
			{
				Program.WriteMessage(MessageType.Warning, e.Message);	
			}
			
			return val;
		}
				
		#endregion
		
		#region Instance Data		
		
		private bool showHelp;
		private ParsedPath scriptPath;
		private string[] arguments;
		private ParsedPath[] references = null;
		private bool debug;
		private bool cacheImages;
		private bool searchSystemPath;
		private bool allowUnsafeCode;

		#endregion
	}

	public class Program
	{
		public static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) 
		{
			// Assembly resolver, used to load assemblies specified in the .rsp files or command line which would 
			// not normally be found because they are not in the GAC, csr.exe directory or cached image directory.

			ParsedPath scriptRspFile = ScriptEnvironment.ScriptRspPath;
			ParsedPath sharedRspFile = ScriptEnvironment.SharedRspPath;
			ParsedPath runtimeRspFile = ScriptEnvironment.RuntimeRspPath;

			ArrayList refPaths = new ArrayList();

			// This will always be null if running in the other appdomain, because we don't cache when
			// the command line option is specified.
			if (ScriptEnvironment.AdditionalReferences != null)
			{
				refPaths.AddRange(ScriptEnvironment.AdditionalReferences);
			}

			if (File.Exists(scriptRspFile))
			{
				refPaths.AddRange(GrabRspFileReferences(scriptRspFile));
			}

			if (File.Exists(sharedRspFile))
			{
				refPaths.AddRange(GrabRspFileReferences(sharedRspFile));
			}

			if (File.Exists(runtimeRspFile))
			{
				refPaths.AddRange(GrabRspFileReferences(runtimeRspFile));
			}

			string assemblyName = args.Name.Substring(0, args.Name.IndexOf(','));

			foreach (ParsedPath path in refPaths)
			{
				if (CaseInsensitiveComparer.Default.Compare(path.File, assemblyName) == 0)
					// Load this assembly, but first fully qualify the path to it
					return Assembly.LoadFrom(path.MakeFullPath(scriptRspFile));
			}
			
			return null;
		}

		internal static void WriteMessage(MessageType s, string format, params object [] args)
		{
			switch (s)
			{
				default:
				case MessageType.Normal:
					Console.Out.WriteLine(format, args);
					return;
				case MessageType.Debug:
					Console.ForegroundColor = ConsoleColor.DarkGray;
					Console.Error.Write(CsrResources.Debug);
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
				case MessageType.Warning:
					Console.ForegroundColor = ConsoleColor.Yellow;
					Console.Error.Write(CsrResources.Warning);
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
				case MessageType.Error:
					Console.ForegroundColor = ConsoleColor.Red;
					Console.Error.Write(CsrResources.Error);
					Console.Error.WriteLine(format, args);
					Console.ResetColor();
					break;
			}
		}

		[LoaderOptimization(LoaderOptimization.MultiDomain)]
		[STAThread()]
		public static int Main(string[] args)
		{
			#if DEBUG
			if (File.Exists("Csr.exe.BreakOnEntry.flag"))
				Debugger.Break();
			#endif
			
			ProgramOptions options = new ProgramOptions();
			
			if (!ProcessCommandLine(options, args))
				return 1;

			return CompileAndRun(options);
		}

		private static bool ProcessCommandLine(ProgramOptions options, string[] args)
		{
			CommandLineParser parser = new CommandLineParser(
				typeof(ProgramOptions), typeof(CsrResources));

			try
			{
				parser.Parse(args);
				parser.SetTarget(options);

				// If the help option appears anywhere, just show the help
				if (options.ShowHelp)
				{
					Console.Write(parser.LogoBanner);
					Console.Write(parser.Usage);

					WriteLineWithWordBreaks(CsrResources.WithNoSwitchesSpecified);

					return false;
				}

				// At this point, we need a source file (the default argument)
				if (options.ScriptPath == null)
					throw new CommandLineArgumentException(CsrResources.NoScriptSpecified);

				// Fully qualify the source
				options.ScriptPath = options.ScriptPath.MakeFullPath();

				if (options.ScriptPath.File == String.Empty)
					throw new CommandLineArgumentException(CsrResources.NoEmptyFileName);
				else if (options.ScriptPath.HasWildcards)
					throw new CommandLineArgumentException(CsrResources.NoScriptWildcards);

				if (options.ScriptPath.Extension == String.Empty)
					options.ScriptPath = new ParsedPath(options.ScriptPath.RootDirectoryAndFile + ".csr");
			}
			catch (CommandLineArgumentException ex)
			{
				Console.Error.WriteLine();
				WriteMessage(MessageType.Error, "{0}", ex.Message);

				return false;
			}
			catch (SystemException ex)
			{
				WriteMessage(MessageType.Error, "{0}\n", ex.Message);
				return false;
			}

			// If there are any command line references, fully qualify them here
			if (options.References.Length != 0)
			{
				for (int i = 0; i < options.References.Length; i++)
				{
					options.References[i] = options.References[i].MakeFullPath(options.ScriptPath);
				}
			}

			return true;
		}

		private static int CompileAndRun(ProgramOptions options)
		{
			// Set publicly visible script path (in this AppDomain).  This also sets the script.rsp file name.
			ScriptEnvironment.ScriptPath = options.ScriptPath;

			ParsedPath localRspFile = ScriptEnvironment.ScriptRspPath;
			ParsedPath sharedRspFile = ScriptEnvironment.SharedRspPath;
			ParsedPath runtimeRspFile = ScriptEnvironment.RuntimeRspPath;

			if (options.Debug)
			{
				WriteMessage(MessageType.Debug, CsrResources.LocalResponseFilePath, localRspFile);
				WriteMessage(MessageType.Debug, CsrResources.SharedResponseFilePath, sharedRspFile);
				WriteMessage(MessageType.Debug, CsrResources.RuntimeResponseFilePath, runtimeRspFile);
			}

			// Check that the source exists, and optionally search for it in the PATH
			if (!File.Exists(options.ScriptPath))
			{
				if (options.SearchSystemPath)
				{
					string [] found = PathUtility.FindFileInPaths(
						System.Environment.GetEnvironmentVariable("PATH").Split(new char[] {Path.PathSeparator}), options.ScriptPath.FileAndExtension);
					
					if (found.Length > 0)
					{
						options.ScriptPath = new ParsedPath(found[0]);
						ScriptEnvironment.ScriptPath = options.ScriptPath;
					
						if (options.Debug)
							WriteMessage(MessageType.Debug, CsrResources.ScriptFoundInPath, 
								options.ScriptPath.FileAndExtension, options.ScriptPath.RootAndDirectory);
					}
					else
					{
						WriteMessage(MessageType.Error, CsrResources.ScriptNotFoundInDirectoryOrPath, 
							options.ScriptPath.FileAndExtension, options.ScriptPath.RootAndDirectory);
						return 1;
					}
				}
				else
				{
					WriteMessage(MessageType.Error, CsrResources.ScriptNotFound, options.ScriptPath);
					return 1;
				}
			}

			string cacheFile = null;

			if (options.CacheImages && options.References.Length == 0)
			{
				string cacheDir = ScriptEnvironment.CachePath.RootAndDirectory;
				
				if (!Directory.Exists(cacheDir))
					Directory.CreateDirectory(cacheDir);

				cacheFile = cacheDir + ScriptEnvironment.CachifySourceName(options.ScriptPath);
				
				// Cache file must exist and be newer than the source and all the response files
				if (File.Exists(cacheFile))
				{ 
					bool useCachedFile;
					string csrExeConfig = Assembly.GetExecutingAssembly().Location + ".config";
					
					try
					{
						DateTime cacheLastWriteTime = File.GetLastWriteTime(cacheFile);
						DateTime sourceLastWriteTime = File.Exists(options.ScriptPath) ? 
							File.GetLastWriteTime(options.ScriptPath) : DateTime.MinValue;
						DateTime sourceRspLastWriteTime = File.Exists(localRspFile) ? 
							File.GetLastWriteTime(localRspFile) : DateTime.MinValue;
						DateTime sharedRspLastWriteTime = File.Exists(sharedRspFile) ? 
							File.GetLastWriteTime(sharedRspFile) : DateTime.MinValue;
						DateTime runtimeRspLastWriteTime = File.Exists(runtimeRspFile) ? 
							File.GetLastWriteTime(runtimeRspFile) : DateTime.MinValue;
						DateTime csrExeConfigLastWriteTime = File.Exists(csrExeConfig) ? 
							File.GetLastWriteTime(csrExeConfig) : DateTime.MinValue;
					
						useCachedFile = (
							cacheLastWriteTime > sourceLastWriteTime &&
							cacheLastWriteTime > sourceRspLastWriteTime && 
							cacheLastWriteTime > sharedRspLastWriteTime &&
							cacheLastWriteTime > runtimeRspLastWriteTime &&
							cacheLastWriteTime > csrExeConfigLastWriteTime);
						
						// TODO: Put a message here showing what caused the cache decision
					}
					catch (SystemException)
					{
						// If there was any weirdness checking file information, we won't use the cache
					
						if (options.Debug)
							WriteMessage(MessageType.Warning, CsrResources.ProblemCheckingWriteTimes);
							
						useCachedFile = false;
					}
									
					if (useCachedFile)
					{
						AppDomain domain = AppDomain.CreateDomain("CsrDomain");
						
						domain.SetData("ScriptPath", options.ScriptPath);
						domain.AssemblyResolve += new ResolveEventHandler(OnAssemblyResolve);
						
						try
						{
							return domain.ExecuteAssembly(
								cacheFile, AppDomain.CurrentDomain.Evidence, options.Arguments);
						}
						catch (Exception ex)
						{
							if (ex is FileLoadException)
							{
								// If we get here, it is most likely because an assembly that the source was originally compiled with has changed.
								// We need to tell the user to delete the cached image and re-try.
								WriteMessage(MessageType.Error, String.Format(CsrResources.FileNotFoundRunningCachedImage, cacheFile));
							}
							else
							{
								// Here, unlike when we are compiling below, the exception from the script is not wrapped
								WriteMessage(MessageType.Error, String.Format(CsrResources.ExceptionFromScript, ex.Message));

								if (options.Debug)
									WriteMessage(MessageType.Debug, Environment.NewLine + ex.StackTrace);
							}
							
							// We used to continue here, but it is not such great idea.  If an exception occurs halfway through the script, 
							// we'll rerun the script again and possibly cause very confusing behavior.  
							return 1;
						}
					}
				}
			}

			// Pass the references this way as it allows the AssemblyResolve method to work in the two appdomain scenario also
			ScriptEnvironment.AdditionalReferences = options.References;
			
			// Combine all the .rsp's with fully qualified paths to all references
			ParsedPath combinedRspFile = CombineResponseFiles(options.References, localRspFile, sharedRspFile, runtimeRspFile);
			
			try
			{
				CodeDomProvider provider = CodeDomProvider.CreateProvider("c#");
				CompilerParameters parms = new CompilerParameters();

				parms.CompilerOptions = "@\"" + combinedRspFile + "\" ";  // Don't forget space at end...
				parms.GenerateExecutable = true;	// This doesn't mean generate a file on disk!
			
				if (cacheFile != null)
				{
					parms.GenerateInMemory = false;
					parms.IncludeDebugInformation = true;
					parms.OutputAssembly = cacheFile;
				}
				else
				{
					parms.GenerateInMemory = true;
				}
			
				if (options.AllowUnsafeCode)
					parms.CompilerOptions += "/unsafe+ ";
				
				CompilerResults results = provider.CompileAssemblyFromFile(parms, options.ScriptPath);
		
				File.Delete(combinedRspFile);
		
				if (results.Errors.Count > 0)
				{
					foreach(CompilerError error in results.Errors)
					{
						Console.Error.WriteLine(CsrResources.ErrorLine, 
							error.FileName, error.Line, error.Column, error.ErrorNumber, error.ErrorText);
					}
				}
				else
				{	
					// Take the compiled assembly and invoke it in this appdomain.
					object ret = null;
					
					AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(OnAssemblyResolve);

					// Get information about the entry point							
					MethodInfo mainMethod = results.CompiledAssembly.EntryPoint;
					ParameterInfo[] mainParams = mainMethod.GetParameters();
					Type returnType = mainMethod.ReturnType;
					
					try
					{
						if (returnType == typeof(void))
						{
							if (mainParams.Length > 0)
								mainMethod.Invoke(null, new object[1] { options.Arguments });
							else
								mainMethod.Invoke(null, null);		
						}
						else
						{
							if (mainParams.Length > 0)
								ret = mainMethod.Invoke(null, new object[1] { options.Arguments });
							else
								ret = mainMethod.Invoke(null, null);
						}
					}
					catch (Exception ex)  // Catch script errors
					{
						// When catching a script error here, the actual script exception will be the inner exception
						WriteMessage(MessageType.Error, String.Format(CsrResources.ExceptionFromScript,
							ex.InnerException != null ? ex.InnerException.Message : ex.Message));

						if (options.Debug && ex.InnerException != null)
							WriteMessage(MessageType.Debug, Environment.NewLine + ex.InnerException.StackTrace);
					}
					
					if (ret != null && returnType == typeof(int))
					{
						return (int)ret;
					}
				}
			}
			catch (Exception ex)  // Catch compilation errors
			{
				string message = CsrResources.ExceptionDuringCompile + ex.Message;

				if (ex.InnerException != null)
					message += ex.InnerException.Message;
				
				WriteMessage(MessageType.Error, "{0} ", message);
				return 1;
			}
			
			return 0;
		}

		private static ParsedPath CombineResponseFiles(
			ParsedPath[] references, ParsedPath scriptRspFile, ParsedPath sharedRspFile, ParsedPath runtimeRspFile)
		{
			// This function combines existing responses file into one temporary one
			// but with all the referenced assemblies having fully qualified paths, relative to the 
			// original response files location.  Fully qualified references are not adjusted.

			ParsedPath combinedRspFile = new ParsedPath(Path.GetTempFileName());

			ParsedPath[] rspFilePaths = new ParsedPath[3] 
			{
				scriptRspFile, sharedRspFile, runtimeRspFile 
			};

			using (StreamWriter wr = File.CreateText(combinedRspFile))
			{
				foreach (ParsedPath refPath in references)
				{
					wr.Write("/r:\"");
					wr.Write(refPath);
					wr.Write("\"\n");
				}

				foreach (ParsedPath rspFilePath in rspFilePaths)
				{
					if (!File.Exists(rspFilePath))
						continue;

					ArrayList refPaths = GrabRspFileReferences(rspFilePath);

					foreach (ParsedPath refPath in refPaths)
					{
						wr.Write("/r:\"");
						wr.Write(refPath.MakeFullPath(rspFilePath));
						wr.Write("\"\n");
					}
				}
			}

			return combinedRspFile;
		}

		private static ArrayList GrabRspFileReferences(ParsedPath rspFile)
		{
			string text;
			StringBuilder sb = new StringBuilder();

			using (StreamReader reader = new StreamReader(rspFile))
			{
				text = reader.ReadToEnd();
			}

			Regex r = new Regex("/r[^:]*:(.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);

			Match m = r.Match(text);

			ArrayList paths = new ArrayList();

			while (m.Success)
			{
				string s = m.Groups[1].Value;

				// Chop off trailing '\r'; won't be there if last line of file doesn't end in \r\n
				if (s[s.Length - 1] == '\r')
					s = s.Substring(0, s.Length - 1);

				s = Environment.ExpandEnvironmentVariables(s);
				
				paths.Add(new ParsedPath(s).MakeFullPath(rspFile));

				m = m.NextMatch();
			}

			return paths;
		}

		/// <summary>
		/// Writes a string out, inserting <see cref="Environment.NewLine"/> after the last full word
		/// in each console line.  Uses <see cref="StringUtility.WordBreak"/> to insert the line breaks.  Uses the line 
		/// length of the process console.  If there is  no console allocated for the current process, a default line 
		/// length of 79 characters is used.
		/// </summary>
		/// <param name="text"></param>
		public static void WriteLineWithWordBreaks(string text)
		{
			int bufferWidth = 80;
			
			// This might fail if there is no console, and there is no way to check in advance
			try
			{
				bufferWidth = Console.BufferWidth;
			}
			catch (IOException) {}
		
			int lineLength = bufferWidth < 2 ? 79 : bufferWidth - 1;

			string[] lines = StringUtility.WordBreak(text, lineLength);

			WriteLines(lines);
		}

		/// <summary>
		/// Writes an array of lines out.  Useful when combined with <see cref="StringUtility.WordBreak"/>
		/// </summary>
		/// <param name="lines"></param>
		public static void WriteLines(string[] lines)
		{
			foreach (string line in lines)
				Console.WriteLine(line);
		}
	}
}
